在上一篇中,我們成功地使用 ConfigFS 將 Raspberry Pi 打造成了一個支援 Mass Storage (大容量儲存) 以及 CDC ACM (虛擬序列埠) 的 USB 設備。理論上,當你把 Pi 插上 Windows 電腦時,應該會同時跳出一個隨身碟,並且在裝置管理員裡看到一個 COM Port。
但現實總是殘酷的。很多開發者第一次嘗試建立 Composite Device (複合設備) 時,最常遇到的情況是:隨身碟抓到了,但 COM Port 卻顯示黃色驚嘆號(無法辨識的 USB 裝置),或者反過來。
這篇文章,我們將深入探討 USB 複合設備的列舉 (Enumeration) 過程,以及為什麼在 Linux ConfigFS 中,一個看似無關緊要的「建立捷徑 (Symlink) 順序」,會成為決定 Windows 驅動程式生死存亡的黃金法則。
[問題情境] 為什麼總是少一個裝置?
當我們透過 Bash 腳本設定 ConfigFS 時,通常會建立兩個 functions(例如 mass_storage.usb0 與 acm.usb0),然後將它們分別連結 (symlink) 到 USB configuration 中:
bash
# 建立 Mass Storage 與 ACM
mkdir -p functions/mass_storage.usb0
mkdir -p functions/acm.usb0
# (中間省略屬性設定...)
# 建立 Symlink 綁定到組態
ln -s functions/mass_storage.usb0 configs/c.1/
ln -s functions/acm.usb0 configs/c.1/
當你興高采烈地執行腳本並將 Pi 連上 Windows 電腦時,打開裝置管理員,你可能會看到:
USB 大容量儲存裝置 (正常運作)
序列 USB 裝置 (COM3) (正常運作)
但如果你有一天不小心對調了 ln -s 的順序,改成先連結 acm.usb0 再連結 mass_storage.usb0,你可能會驚訝地發現 Windows 突然認不出這個設備,或者一直跳出「驅動程式安裝失敗」的警告!
為什麼只是幾行 Linux 的掛載順序,會影響到 Windows 的驅動行為?
[底層原理] USB 複合設備的列舉機制
要理解這個問題,我們需要稍微潛入 USB 協議的底層。
當一個 USB 裝置插入主機 (Host, 在我們的場景中是 Windows 電腦) 時,Host 會發送標準請求來獲取裝置的描述符 (Descriptors)。對於複合設備而言,Host 會取得一個包含多個介面 (Interfaces) 的 Configuration Descriptor。
Windows 的 IAD (Interface Association Descriptor) 陷阱
USB CDC (Communication Device Class) 是一個非常特殊的類別。一個標準的 CDC ACM 虛擬序列埠,實際上是由兩個 Interfaces 組成的:
Communication Class Interface (CCI):負責控制指令 (如 Baud Rate 設定)。
Data Class Interface (DCI):負責實際的資料傳輸 (RX/TX)。
當 Windows 遇到一個複合設備(擁有多種功能,例如 CDC + Mass Storage),它需要知道這多個 Interfaces 中,哪幾個是屬於同一個功能的。為此,USB 規範引入了 IAD (Interface Association Descriptor)。
在 Linux ConfigFS 中,當你把 function symlink 到 configs/c.1/ 時,Linux 核心會根據你 symlink 的順序,依序指派 Interface Number (0, 1, 2, 3...)。
黃金法則:CDC 必須連續且優先排在前面嗎?
其實,Windows 的通用 USB 驅動 ( usbccgp.sys ) 對於 Interface 的解析非常嚴格(有時甚至有點 Buggy)。 如果你的 Symlink 順序是:
mass_storage (佔用 Interface 0)
acm (佔用 Interface 1 與 Interface 2)
在某些舊版 Windows 10 或 Windows 7 系統中,如果 CDC 不是排在 Interface 0 開始,IAD 的解析可能會失敗,導致 Windows 無法正確加載 usbser.sys (序列埠驅動程式),最終變成黃色驚嘆號。
反之,如果我們強制讓 CDC 先註冊:
acm (佔用 Interface 0 與 1)
mass_storage (佔用 Interface 2) Windows 解析 IAD 時就能完美辨識前兩個 Interface 是一個完整的 COM Port,剩下的則是隨身碟。
[最終解決方案] ConfigFS 腳本的正確排版
為了一勞永逸地解決所有跨平台(Windows / Mac / Linux)的相容性問題,我們在編寫 usb_setup.sh 腳本時,必須嚴格遵守以下順序:
先建立並設定好所有的 Functions。
在綁定 (Symlink) 階段,永遠讓 CDC ACM (序列埠) 優先綁定。
最後再綁定 Mass Storage (隨身碟) 或其他功能。
以下是經過實機壓力測試 (Production Verified) 的正確配置順序:
bash
#!/bin/bash
# 去敏化後的 EdgeNode USB ConfigFS 初始化腳本
CONFIGFS_HOME="/sys/kernel/config/usb_gadget/edgenode"
mkdir -p $CONFIGFS_HOME
cd $CONFIGFS_HOME
# 1. 設定基本 USB 屬性 (VID/PID 必須合法)
echo 0x1D6B > idVendor # Linux Foundation
echo 0x0104 > idProduct # Multifunction Composite Gadget
echo 0x0200 > bcdUSB
echo 0x0100 > bcdDevice
# 2. 建立 CDC ACM (序列埠)
mkdir -p functions/acm.usb0
# 3. 建立 Mass Storage (隨身碟)
mkdir -p functions/mass_storage.usb0
echo "/opt/gateway/storage.bin" > functions/mass_storage.usb0/lun.0/file
echo 1 > functions/mass_storage.usb0/lun.0/removable
echo 0 > functions/mass_storage.usb0/lun.0/cdrom
# 4. 建立組態 (Configuration)
mkdir -p configs/c.1
echo 250 > configs/c.1/MaxPower
# ==========================================
# ⚠️ 關鍵區塊:Symlink 的順序決定了 Windows 驅動的成敗
# ==========================================
# 第一步:先綁定 ACM (確保它佔用 Interface 0 & 1)
ln -s functions/acm.usb0 configs/c.1/
# 第二步:再綁定 Mass Storage (佔用 Interface 2)
ln -s functions/mass_storage.usb0 configs/c.1/
# ==========================================
# 5. 點火啟動 (綁定 UDC)
ls /sys/class/udc > UDC
TIP
為什麼 Mac 和 Linux 通常沒這個問題? macOS 與 Linux 核心的 USB 探測機制 (Probing) 更加彈性與現代化,它們能夠動態掃描整個 Descriptor Tree 並根據 Class Code 自動配對驅動,而不太受限於 Interface 的絕對順序。這也是為什麼很多開發者在 Mac 上開發測試都正常,一交給客戶的 Windows 筆電就當機的原因。
總結
「在 ConfigFS 中,ln -s 的執行順序就是硬體介面的物理順序。」
這個看似簡單的 Linux 指令,在邊緣設備開發中卻掌握著 Windows 驅動的生殺大權。透過將 CDC ACM 排在首位,我們成功避開了 Windows usbccgp.sys 的解析地雷,打造出隨插即用的完美複合設備。
解決了 USB 的硬體層通訊後,我們將面臨下一個更巨大的挑戰:檔案系統崩潰。當樹莓派和 Windows 同時想對這顆虛擬隨身碟寫入資料時,會發生什麼慘劇?明天,我們將進入第三階段:檔案系統的生存遊戲。